package dnl.jexem.watchdog;

import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;

import org.apache.commons.io.filefilter.IOFileFilter;
import org.apache.commons.io.filefilter.RegexFileFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;

import dnl.confij.ConfigException;
import dnl.confij.Configuration;
import dnl.confij.ini.IniConfiguration;
import dnl.confij.reflected.ReflectedConfiguration;
import dnl.infra.cli.CLI;
import dnl.jexem.env.WatchedProcess;
import dnl.jexem.env.lifesign.LifeSign;
import dnl.jexem.env.lifesign.ProcessLifeSignCheck;
import dnl.jexem.watchdog.cli.ConfigDirSetting;
import dnl.jexem.watchdog.cli.DebugSetting;
import dnl.jexem.watchdog.restarts.RestartInfoService;
import dnl.utils.cp.ClassPathHacker;
import dnl.utils.mail.MailAccount;
import dnl.utils.mail.MailSender;
import dnl.utils.mail.MessageDetails;

/**
 * 
 * @author Daniel Orr
 * 
 */
public class WatchDog extends Thread {

	private static final int DEFAULT_WATCH_INTERVAL = 20 * 60 * 1000;
	private static Logger logger = LoggerFactory.getLogger(WatchDog.class);

	private List<WatchedProcess> watchedProcesses = new ArrayList<WatchedProcess>();

	private final HistoryLog historyLog = new HistoryLog();

	private long watchIntervalMs = DEFAULT_WATCH_INTERVAL;

	private MailSender mailSender;
	private MailAccount mailAccount;
	private MailConfiguration mailConfiguration;
	private File configDir;

	private RestartInfoService restartInfoService;
	
	/**
	 * Constructs a <code>WatchDog</code> that watches over the given
	 * <code>WatchedProcess</code>es.
	 * 
	 * @param watchedProcesses
	 *            a <code>List</code> of processes to watch on.
	 */
	public WatchDog(List<WatchedProcess> watchedProcesses) {
		this.watchedProcesses = watchedProcesses;
	}

	/**
	 * Constructs a <code>WatchDog</code> that watches over the
	 * <code>WatchedProcess</code>es that are defined in applicationContext*.xml
	 * that are located under the given configuration directory.
	 * 
	 * @param configDir
	 *            the directory under which all watchdog configuration files are
	 *            located.
	 * 
	 * @param debug
	 *            if <code>true</code> restarts will be logged but not performed
	 *            actually.
	 */
	public WatchDog(File configDir, boolean debug) {
		this.configDir = configDir;
		loadClasspath();
		loadApplicationContexts(debug);
		loadConfiguration();
	}

	public void setRestartOccurenceService(RestartInfoService restartInfoService) {
		this.restartInfoService = restartInfoService;
	}

	public void setNotificationsMailAccount(MailAccount mailAccount) {
		this.mailSender = new MailSender();
		this.mailAccount = mailAccount;
	}

	public long getWatchIntervalMs() {
		return watchIntervalMs;
	}

	public void setWatchIntervalMs(long watchIntervalMs) {
		this.watchIntervalMs = watchIntervalMs;
	}

	public void addWatch(WatchedProcess watchedProcess) {
		synchronized (watchedProcess) {
			watchedProcesses.add(watchedProcess);
		}
	}

	private void loadConfiguration() {
		File config = new File(configDir, "watchdog.config.ini");
		Configuration iniConfig;
		try {
			iniConfig = new IniConfiguration(config);
			if (iniConfig.getSection("mail") == null) {
				return;
			}
			ReflectedConfiguration rConfig = new ReflectedConfiguration(iniConfig);
			mailConfiguration = new MailConfiguration();
			rConfig.registerObject("mail", mailConfiguration);

			this.mailAccount = new MailAccount();
			this.mailAccount.setHostName(mailConfiguration.getHostName());
			this.mailAccount.setPasswordImpl(mailConfiguration.getPassword());
			this.mailAccount.setSendAddress(mailConfiguration.getSendAddress());
			this.mailAccount.setSmtpPort(mailConfiguration.getSmtpPort());
			this.mailAccount.setUser(mailConfiguration.getUser());
			this.mailSender = new MailSender();
		} catch (ConfigException e) {
			e.printStackTrace();
		}
	}

	@SuppressWarnings("unchecked")
	private void loadApplicationContexts(boolean debug) {
		IOFileFilter fileFilter = new RegexFileFilter("applicationContext.*xml");
		File[] files = configDir.listFiles((FileFilter) fileFilter);
		String[] fileUrls = new String[files.length];
		for (int i = 0; i < fileUrls.length; i++) {
			try {
				fileUrls[i] = files[i].toURI().toURL().toString();
			} catch (MalformedURLException e) {
				e.printStackTrace();
			}
		}
		FileSystemXmlApplicationContext applicationContext = new FileSystemXmlApplicationContext(
				fileUrls);

		Map<?, ?> managedProcessesMap = applicationContext.getBeansOfType(WatchedProcess.class);
		Collection<WatchedProcess> managedProcesses = (Collection<WatchedProcess>) managedProcessesMap
				.values();

		if (debug) {
			for (WatchedProcess watchedProcess : managedProcesses) {
				watchedProcess.setDebug(true);
			}
		}
		this.watchedProcesses = new ArrayList<WatchedProcess>(managedProcesses);
	}

	private void loadClasspath() {
		File classpathConfig = new File(configDir, "classpath.config");
		if (classpathConfig.exists()) {
			logger.info("loading classpath from: {}", classpathConfig.getAbsolutePath());
			try {
				ClassPathHacker.load(new FileReader(classpathConfig));
			} catch (FileNotFoundException e) {
				e.printStackTrace();
			}
		}
	}

	public void run() {
		while (true) {
			try {
				for (WatchedProcess watchedProcess : watchedProcesses) {
					logger.info("Checking process '{}'", watchedProcess.getName());
					List<LifeSign> lifeSigns = watchedProcess.getLifeSigns();
					if (lifeSigns.isEmpty()) {
						logger.info(
								"Process '{}' has no registered life signs. Adding a default one.",
								watchedProcess.getName());
						watchedProcess.addLifeSign(new ProcessLifeSignCheck(watchedProcess));
					}
					for (LifeSign lifeSign : lifeSigns) {
						checkLifeSign(watchedProcess, lifeSign);
					}
				}
			} catch (Exception e) {
				logger.error("Severe exception: {}", e.getMessage());
			}
			try {
				Thread.sleep(watchIntervalMs);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}

	private void checkLifeSign(WatchedProcess watchedProcess, LifeSign lifeSign) {
		if (!lifeSign.isAlive()) {
			logger.info(
					"Life sign {} check for process '{}' failed. Restarting ...",
					lifeSign.getClass().getSimpleName(), watchedProcess.getName());
			HistoryLogEntry logEntry = new HistoryLogEntry();
			logEntry.setManagedProcessName(watchedProcess.getName());
			logEntry.setRestartMessage(lifeSign.getClass().getSimpleName());
			logEntry.setRestartTime(new Date());
			historyLog.addLog(logEntry);
			sendRestartNotification(watchedProcess.getName(), lifeSign.getClass()
					.getName());
			persistRestartInfo(watchedProcess.getName(), lifeSign.getClass()
					.getName());
			if (historyLog.getRestartsCountFromLast15minuts(watchedProcess
					.getName()) > 2) {
				logger.warn("Process {} is unstable.", watchedProcess.getName());
			}
			watchedProcess.restart();
		} else {
			logger.info("Life sign {} check for process '{}' is positive.",
					lifeSign.getClass().getSimpleName(), watchedProcess.getName());
		}
	}

	private void persistRestartInfo(String processName, String originLifeSign){
		restartInfoService.persistRestartInfo(processName, originLifeSign);
	}
	
	private void sendRestartNotification(String processName, String restartMessage) {
		if(mailConfiguration == null){
			return;
		}
		MessageDetails messageDetails = new MessageDetails();
		messageDetails.setRecipients(Collections.singletonList(mailConfiguration.getRecipient()));
		messageDetails.setSubject("Process restart");
		messageDetails.setContent("Prococess '" + processName + "' was restarted:\nmessage="
				+ restartMessage + "\ntime" + new Date().toString());
		try {
			mailSender.sendEmail(mailAccount, messageDetails);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public static void main(String[] args) {

		CLI cli = new CLI("watchdog");

		DebugSetting debugSetting = new DebugSetting();
		ConfigDirSetting configDirSetting = new ConfigDirSetting();
		cli.addSetting(debugSetting);
		cli.addSetting(configDirSetting);

		cli.processCommandLine(args);

		File configDir = configDirSetting.getConfigDir();
		if (!configDir.isDirectory()) {
			System.err.println("Given path '" + configDir.getAbsolutePath()
					+ "' does not correspond to a directory.");
			System.exit(1);
		}

		WatchDog watchDog = new WatchDog(configDir, debugSetting.isDebug());
		String[] contexts = {
				"classpath:applicationContext.xml",
				"classpath:applicationContext-transaction.xml"
				};
		ApplicationContext applicationContext = new ClassPathXmlApplicationContext(contexts);
		Object restartInfoService = applicationContext.getBean("restartInfoService");
		watchDog.setRestartOccurenceService((RestartInfoService) restartInfoService);
		watchDog.start();
	}
}
